# 40. 序列化模块,json、pickle、shelve

# 序列化模块

序列化分为二部分,序列化(正序列化)、反序列化

  1. 序列化(正序列化):将原本的字典、列表等一些类型的内容转成一个字符串的过程
  2. 反序列化:将转成的序列化字符串转换回原本的字典、列表等一些类型的内容的过程

# 那为什么要有序列化模块

比如,我们在python代码中计算的一个数据需要给另外一段程序使用,那我们怎么给?

现在我们能想到的方法就是存在文件里,然后另一个python程序再从文件里读出来。

但是我们都知道,对于文件来说是没有字典这个概念的,所以我们只能将数据转换成字典放到文件中。

你一定会问,将字典转换成一个字符串很简单,就是str(dic)就可以办到了,为什么我们还要学习序列化模块呢?

没错序列化的过程就是从dic 变成str(dic)的过程。现在你可以通过str(dic),将一个名为dic的字典转换成一个字符串,

但是你要怎么把一个字符串转换成字典呢?

聪明的你肯定想到了eval(),如果我们将一个字符串类型的字典str_dic传给eval,就会得到一个返回的字典类型了。

eval()函数十分强大,但是eval是做什么的?

python官方demo解释为:将字符串str当成有效的表达式来求值并返回计算结果。

BUT!强大的函数有代价。安全性是其最大的缺点。

想象一下,如果我们从文件中读出的不是一个数据结构,而是一句"删除文件"类似的破坏性语句,那么后果实在不堪设设想。

而使用eval就要担这个风险。

所以,我们并不推荐用eval方法来进行反序列化操作(将str转换成python中的数据结构)

# 序列化的目的

  1. 以某种存储形式使自定义对象持久化(持久化:将其写入磁盘、数据库等永久存储的方式)
  2. 将对象从一个地方传递到另一个地方
  3. 使程度更具维护性
  4. 序列化都是,数据转成序列化(字符串),序列化(字符串)转成数据

# 序列化的三大模块

json模块

  1. 支持所有语言,拥有跨语言的优势
  2. 支持的数据类型有限
  3. 限制比较多

pickle模块

  1. 序列化后的数据都是bytes类型,想要查看就只能反序列后在看
  2. 支持几乎所有对象的序列化

shelve模块

  1. 不常用
  2. 在3.x 版本的Python,shelve只读选项不支持,目前是否支持不清楚
  3. 需要大量范围的读取类字典格式的程序,才推荐使用
  4. 一般推荐使用json模块跟pickle模块

# json模块

json一共只提供四个方法:dumps、dump、losds、load

注意:json转换完的字符串类型都是用 " " 来表示

# dumps序列化方法

注意:json转换完的字符串类型都是用 " " 来表示

import json
so = {1:'a',2:'b',3:'c',4:'e',5:'f',6:'g'}
print(json.dumps(so))
print([so])
print([json.dumps(so)])

执行结果:
{"1": "a", "2": "b", "3": "c", "4": "e", "5": "f", "6": "g"}
[{1: 'a', 2: 'b', 3: 'c', 4: 'e', 5: 'f', 6: 'g'}]
['{"1": "a", "2": "b", "3": "c", "4": "e", "5": "f", "6": "g"}']

## 从执行结果就能看出来区别了吧

# loads反序列化方法

注意:字典的key值转成序列化前都是int类型,转后就成字符串类型,在反序列转后就还是字符串,只是 "" 换成了 ''

import json
so = {1:'a',2:'b',3:'c',4:'e',5:'f',6:'g'}
so = json.dumps(so)
print(json.loads(so))
print([json.loads(so)])

执行结果:
{'1': 'a', '2': 'b', '3': 'c', '4': 'e', '5': 'f', '6': 'g'}
[{'1': 'a', '2': 'b', '3': 'c', '4': 'e', '5': 'f', '6': 'g'}]

# dump用于传递的序列化方法

注意:json转换完的字符串类型都是用 " " 来表示

格式:json.dump(要转换的数据,要传递的方式)

import json
so = {1:'a',2:'b',3:'c',4:'e',5:'f',6:'g'}
with open("json_so",mode="w",encoding="utf-8") as f:
    json.dump(so,f)
    
## 执行后文件内容
{"1": "a", "2": "b", "3": "c", "4": "e", "5": "f", "6": "g"}

# load用于传递的反序列化方法

注意:字典的key值转成序列化前都是int类型,转后就成字符串类型,在反序列转后就还是字符串,只是 "" 换成了 ''

格式:json.dump(要传递的方式)

import json
so = {1:'a',2:'b',3:'c',4:'e',5:'f',6:'g'}
with open("json_so",mode="r",encoding="utf-8") as f:
    so = json.load(f)
print(so)
print([so])
print(type(so))

执行结果:
{'1': 'a', '2': 'b', '3': 'c', '4': 'e', '5': 'f', '6': 'g'}
[{'1': 'a', '2': 'b', '3': 'c', '4': 'e', '5': 'f', '6': 'g'}]
<class 'dict'>

# json序列化选项

序列化选项,分常用跟不常用,常用只有一个,哈哈哈哈哈哈

# 常用的序列化选项 - ensure_ascii

ensure_ascii:

  1. True:默认就是True,是否对中文进行编译,默认是
  2. False:不对中文进行编译
so = {1:'a',2:'b',3:'c',4:'e',5:'f',6:'g',7:"中国"}
print(json.dumps(so))
print(json.dumps(so,ensure_ascii=False))

执行结果:
{"1": "a", "2": "b", "3": "c", "4": "e", "5": "f", "6": "g", "7": "\u4e2d\u56fd"}
{"1": "a", "2": "b", "3": "c", "4": "e", "5": "f", "6": "g", "7": "中国"}

# 其他不常用的选项

  1. Skipkeys:默认值是False,如果dict的keys内的数据不是python的基本类型(str,unicode,int,long,float,bool,None),设置为False时,就会报TypeError的错误。此时设置成True,则会跳过这类key
  2. ensure_ascii:,当它为True的时候,所有非ASCII码字符显示为\uXXXX序列,只需在dump时将ensure_ascii设置为False即可,此时存入json的中文即可正常显示。)
  3. indent:应该是一个非负的整型,如果是0就是顶格分行显示,如果为空就是一行最紧凑显示,否则会换行且按照indent的数值显示前面的空白分行显示,这样打印出来的json数据也叫pretty-printed json
  4. separators:分隔符,实际上是(item_separator, dict_separator)的一个元组,默认的就是(‘,’,’:’);这表示dictionary内keys之间用“,”隔开,而KEY和value之间用“:”隔开。
  5. sort_keys:将数据根据keys的值进行排序
import json
data = {'username':['李华','二愣子'],'sex':'male','age':16}
print(json.dumps(data,sort_keys=True,indent=2,separators=(',',':'),ensure_ascii=False))
so = {1:'a',2:'b',3:'c',4:'e',5:'f',6:'g',7:'中国'}
print(json.dumps(so,sort_keys=True,indent=2,separators=(':',','),ensure_ascii=False))

执行结果:
{
  "age":16,
  "sex":"male",
  "username":[
    "李华",
    "二愣子"
  ]
}
{
  "1","a":
  "2","b":
  "3","c":
  "4","e":
  "5","f":
  "6","g":
  "7","中国"
}

注意:这样虽然比较好看,也比较看得懂,但是这样进行存储,会大量的消耗容量,如这样进行传递,也会大量的消耗网络资源

# json的限制

json格式的限制:

  1. json格式的key必须是字符串数据类型
  2. json格式中的字符串只能是 " "

# 如果是数字为key,那么dump之后会强行转成字符串类型

dic = {1:2,3:4}
str_dic = json.dumps(dic)
print(str_dic)
new_dic = json.loads(str_dic)
print(new_dic)

执行结果:
{"1": 2, "3": 4}
{'1': 2, '3': 4}

# json是否支持元组

对元组做value的字典会把元组强制转换成列表,反序列后,强制转换的列表,转换不了元组

注意:json不支持元组做为字典的key值,在正常的环境下元组是可以做为字典的key,但是json不支持

dic = {'abc':(1,2,3)}
str_dic = json.dumps(dic)
print(str_dic)
new_dic = json.loads(str_dic)
print(new_dic)
print(type(new_dic["abc"]))

执行结果:
{"abc": [1, 2, 3]}
{'abc': [1, 2, 3]}
<class 'list'>

# 对于序列化到文件的限制

## 把列表序列化写入文件中
lst = ['aaa',123,'bbb',12.456]
with open('json_demo','w') as f:
    json.dump(lst,f)

## 文件的内容
["aaa", 123, "bbb", 12.456]

## 在读取文件内容并反序列
with open('json_demo') as f:
    ret = json.load(f)
    print(ret)

执行结果:
['aaa', 123, 'bbb', 12.456]


## 那如果文件的内容是这样的呢
['aaa', 123, 'bbb', 12.456]

## 在读取文件内容并反序列
with open('json_demo') as f:
    ret = json.load(f)
    print(ret)

执行结果:
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 2 (char 1)

## 那为什么会报错,因为json在反序列的时候不认识 "" 只认识'',如果存在 '' 那么就会报错

# json能多次dump数据到文件里,但是不能load出来了

## 多次向文件写入序列好的数据
dic = {'abc':(1,2,3)}
lst = ['aaa',123,'bbb',12.456]
with open('json_demo','w') as f:
    json.dump(lst,f)
    json.dump(dic,f)

## 文件内容
["aaa", 123, "bbb", 12.456]{"abc": [1, 2, 3]}

## 向读取文件内容并反序列
with open('json_demo') as f:
    ret = json.load(f)
    print(ret)
    
执行结果:
json.decoder.JSONDecodeError: Extra data: line 1 column 28 (char 27)

**注意:**因为在python反序列的时候,只允许一个变量进行反序列,或者只允许一个数据类型的数据进行反序列

# 如果想要json能多次dump数据到文件里,也能load出来了

这里就要用点取巧的方式

## 多次向文件写入序列好的数据
dic = {'abc':(1,2,3)}
lst = ['aaa',123,'bbb',12.456]
with open('json_demo','w') as f:
    str_lst = json.dumps(lst)
    str_dic = json.dumps(dic)
    f.write(str_lst+'\n')
    f.write(str_dic+'\n')

## 文件内容
["aaa", 123, "bbb", 12.456]
{"abc": [1, 2, 3]}

## 向读取文件内容并反序列
with open('json_demo') as f:
    for line in f:
        ret = json.loads(line)
        print(ret)
        
执行结果:
['aaa', 123, 'bbb', 12.456]
{'abc': [1, 2, 3]}

这里用的是,不用dump方式来传递数据,用原始的文件写入功能,写入一行序列化数据后,在写入换行符,在写入一行序列化数据后,在写入换行符等等

这样子,多个序列化都分行存放,那么读取文件的时候,就可以一行一行读取,使用for循环方法,读取一行就反序列化回来,这样就能比较完善的处理需求

# pickle模块

pickle模块有几点是需要注意的

  1. 序列化后的数据都是bytes类型,想要查看就只能反序列后在看
  2. 支持几乎所有对象的序列化
  3. 对于对象的序列化后要进行反序列化,需要这个对象对应的类在内存中
  4. dump出来的结果是bytes类型的数据,所以要使用dump传递到文件中,那么文件的打开方式应该要用 "wb" 的方式打开,那么load呢,读取文件的方式也应该是 "rb" 的方式读取
  5. pickle模块序列化后,在反序化列出的结果,跟原先的一模一样

# dumps序列化方法

注意:dumps序列后的数据都是bytes类型的数据

import pickle
so = ([1,2,"33",{"v1":"n1"},"aaa",["sakl","中国"],{"abc","123"},"中国"])
print(pickle.dumps(so))

执行结果:
b'\x80\x03]q\x00(K\x01K\x02X\x02\x00\x00\x0033q\x01}q\x02X\x02\x00\x00\x00v1q\x03X\x02\x00\x00\x00n1q\x04sX\x03\x00\x00\x00aaaq\x05]q\x06(X\x04\x00\x00\x00saklq\x07X\x06\x00\x00\x00\xe4\xb8\xad\xe5\x9b\xbdq\x08ecbuiltins\nset\nq\t]q\n(X\x03\x00\x00\x00abcq\x0bX\x03\x00\x00\x00123q\x0ce\x85q\rRq\x0eh\x08e.'

## dumps跟dump的结果都是bytes类型的数据,没有什么选项能让他转其他类型的数据,只有通过反序列,load跟loads方法

# loads反序列化方法

import pickle
so = ([1,2,"33",{"v1":"n1"},"aaa",["sakl","中国"],{"abc","123"},"中国"])
so = pickle.dumps(so)
print(pickle.loads(so))

执行结果:
[1, 2, '33', {'v1': 'n1'}, 'aaa', ['sakl', '中国'], {'abc', '123'}, '中国']

# 使用对象进行序列化

import pickle
class coso:
    def __init__(self,name,age):
        self.name = name
        self.age = age
so = coso("江凡",22)
du_so = pickle.dumps(so)
lo_so = pickle.loads(du_so)
print(lo_so.name)
print(lo_so.age)
print(pickle.loads(du_so))
print(pickle.loads(du_so).name)
print(pickle.loads(du_so).age)

执行结果:
江凡
22
<__main__.coso object at 0x0000024681AA5EF0>
江凡
22

# 单次dump到文件跟load文件出来

## 注释类
import pickle
# class coso:
#     def __init__(self,name,age):
#         self.name = name
#         self.age = age
so = coso("江凡",22)
with open("pickle_so","wb") as f:
    pickle.dump(so,f)
with open("pickle_so","rb") as f:
    lo_so = pickle.load(f)
print(lo_so.name)
print(lo_so.age)

执行结果:
NameError: name 'coso' is not defined
## 报找不到这个类


## 不注释类
import pickle
class coso:
    def __init__(self,name,age):
        self.name = name
        self.age = age

so = coso("江凡",22)
with open("pickle_so","wb") as f:
    pickle.dump(so,f)
with open("pickle_so","rb") as f:
    lo_so = pickle.load(f)
print(lo_so.name)
print(lo_so.age)

执行结果:
江凡
22

# 多次dump到文件跟load文件出来

多次dump到文件很成功,但是load就有点限制

load的方式很像生成器,写一行,读一行

import pickle
class coso:
    def __init__(self,name,age):
        self.name = name
        self.age = age
so1 = coso("江凡",22)
so2 = coso("李城",19)
so3 = coso("小燕子",21)
with open("pickle_so","wb") as f:
    pickle.dump(so1, f)
    pickle.dump(so2, f)
    pickle.dump(so3, f)
with open("pickle_so","rb") as f:
    print(pickle.load(f).name)
    print(pickle.load(f).name)
    print(pickle.load(f).name)
    
执行结果:
江凡
李城
小燕子

解决多次dump,load的限制**

import pickle
class coso:
    def __init__(self,name,age):
        self.name = name
        self.age = age
so1 = coso("江凡",22)
so2 = coso("李城",19)
so3 = coso("小燕子",21)
with open("pickle_so","wb") as f:
    pickle.dump(so1, f)
    pickle.dump(so2, f)
    pickle.dump(so3, f)
with open("pickle_so","rb") as f:
    while 1:
        try:
            print(pickle.load(f).name)
        except EOFError:
            break
            
执行结果:
江凡
李城
小燕子

## 通过while无限循环,不断的取值,在使用异常处理,如果报EOFError就退出循环
## 那么EOFError错是怎么来的

import pickle
class coso:
    def __init__(self,name,age):
        self.name = name
        self.age = age
so1 = coso("江凡",22)
so2 = coso("李城",19)
so3 = coso("小燕子",21)
with open("pickle_so","wb") as f:
    pickle.dump(so1, f)
    pickle.dump(so2, f)
    pickle.dump(so3, f)
with open("pickle_so","rb") as f:
    while 1:
        print(pickle.load(f).name)
        
执行结果:
江凡
李城
小燕子
EOFError: Ran out of input
    
## 报错就是项就是这样来的

# dump跟load的用法

这里只介绍pickle模块中的dump跟load的一种用法

## 第一步
import pickle
class coso:
    def __init__(self,name,age):
        self.name = name
        self.age = age
so = coso("江凡",22)
so.name = "李城"
with open("pickle_so","wb") as f:
    pickle.dump(so,f)
with open("pickle_so","rb") as f:
    lo_so = pickle.load(f)
print(lo_so.name)

执行结果:
李城

## 第二步
import pickle
class coso:
    def __init__(self,name,age):
        self.name = name
        self.age = age
so = coso("江凡",22)
print(so.name)
# so.name = "李城"
# with open("pickle_so","wb") as f:
#     pickle.dump(so,f)
with open("pickle_so","rb") as f:
    lo_so = pickle.load(f)
print(lo_so.name)

执行结果:
江凡
李城

## 有没有看出什么,这样子就类似于存档之类的用法

# shelve模块

shelve模块只有二个命令,读取文件跟关闭文件

打开文件并添加数据

shelve模块默认会创建三个文件,文件名.bak、文件名.dat、文件名.dir,这三个文件

import shelve
so = shelve.open("shelve_so")
so["key"] = "value"
so.close()

查看数据

查看数据也是需要打开文件的

import shelve
so = shelve.open("shelve_so")
sh_so = so["key"]
so.close()
print(sh_so)

执行结果:
value

总结

shelve模块存储类似字典方式,一个Key值对应一个value值

查询的时候,跟查看字典一样,输入对应的key值就能查询到对应的value值

不常用,如需用,请自行查阅官网文档